/** * Copyright (C) 2009, 2010 SC 4ViewSoft SRL * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.elasticdroid; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import org.achartengine.ChartFactory; import org.achartengine.GraphicalView; import org.achartengine.chart.PointStyle; import org.achartengine.model.XYMultipleSeriesDataset; import org.achartengine.model.XYSeries; import org.achartengine.renderer.XYMultipleSeriesRenderer; import org.achartengine.renderer.XYSeriesRenderer; import org.elasticdroid.db.ElasticDroidDB; import org.elasticdroid.db.tblinfo.MonitorTbl; import org.elasticdroid.model.CloudWatchMetricsModel; import org.elasticdroid.model.MonitorInstanceModel; import org.elasticdroid.tpl.GenericActivity; import org.elasticdroid.utils.CloudWatchInput; import org.elasticdroid.utils.DialogConstants; import static org.elasticdroid.utils.MonitoringDurations.*; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.text.Html; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.ViewGroup.LayoutParams; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.cloudwatch.model.Datapoint; import com.amazonaws.services.cloudwatch.model.Dimension; public class MonitorInstanceView extends GenericActivity { /** * The AWS connection data */ private HashMap<String, String> connectionData; /** Dialog box for displaying errors */ private AlertDialog alertDialogBox; /** * set to show if alert dialog displayed. Used to decide whether to restore * progress dialog when screen rotated. */ private boolean alertDialogDisplayed; /** message displayed in {@link #alertDialogBox alertDialogBox}. */ private String alertDialogMessage; /** * boolean to indicate if an error that occurred is sufficiently serious to * have the activity killed. */ private boolean killActivityOnError; /** * String holding Instance ID */ private String instanceId; /** * Monitor Instance model */ private MonitorInstanceModel monitorInstanceModel; /** The metrics model */ private CloudWatchMetricsModel metricsModel; /** * The {@link CloudWatchInput} data to provide the model with. */ private CloudWatchInput cloudWatchInput; /** String holding selected region */ private String selectedRegion; /** Data from CloudWatch; returned by {@link MonitorInstanceModel} */ private List<Datapoint> cloudWatchData; /** Cloudwatch metrics; returneed by {@link CloudWatchMetricsModel}*/ private ArrayList<String> measureNames; /** * Tag for logging */ private static final String TAG = "org.elasticdroid.MonitorInstanceView"; //charting stuff: achartengine /** * The dataset to hold the displayed data */ private XYMultipleSeriesDataset dataset; /** * Chart renderer. renders the dataset */ private XYMultipleSeriesRenderer multiRenderer; /** * The chart itself. Added to layout. */ private GraphicalView chartView; /** * Executed when the activity is first (re)created. * @param savedInstanceState Instance state to restore (if any) */ @SuppressWarnings("unchecked") @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = this.getIntent(); try { this.connectionData = (HashMap<String, String>)intent.getSerializableExtra( "org.elasticdroid.EC2SingleInstanceView.connectionData"); } catch(Exception exception) { //the possible exceptions are NullPointerException: the Hashmap was not found, or //ClassCastException: the argument passed is not Hashmap<String, String>. In either case, //just print out the error and exit. This is very inelegant, but this is a programmer's bug Log.e(TAG, exception.getMessage()); finish(); //this will cause it to return to {@link EC2DisplayInstancesView}. } //get instance ID and selected region from the intent this.instanceId = intent.getStringExtra("instanceId"); this.selectedRegion = intent.getStringExtra("selectedRegion"); // create and initialise the alert dialog alertDialogBox = new AlertDialog.Builder(this).create(); // create alert alertDialogBox.setCancelable(false); alertDialogBox.setButton( this.getString(R.string.loginview_alertdialogbox_button), new DialogInterface.OnClickListener() { // click listener on the alert box - unlock orientation when // clicked. // this is to prevent orientation changing when alert box // locked. public void onClick(DialogInterface arg0, int arg1) { alertDialogDisplayed = false; alertDialogBox.dismiss(); // dismiss dialog. // if an error occurs that is serious enough return the // user to the login // screen. THis happens due to exceptions caused by // programming errors and // exceptions caused due to invalid credentials. if (killActivityOnError) { MonitorInstanceView.this.finish(); } } } ); this.setContentView(R.layout.monitorinstance); this.setTitle(connectionData.get("username")+ " (" + selectedRegion +")"); //set title } /** * Restore instance state when the activity is reconstructed after a destroy * * This method restores: * <ul> * <li>cloudWatchInput: The input data (such as period, measure name etc) for the display</li> * </ul> */ @Override public void onRestoreInstanceState(Bundle stateToRestore) { //restore alertDialogDisplayed boolean alertDialogDisplayed = stateToRestore.getBoolean("alertDialogDisplayed"); Log.v(TAG, "alertDialogDisplayed = " + alertDialogDisplayed); alertDialogMessage = stateToRestore.getString("alertDialogMessage"); //was a progress dialog being displayed? Restore the answer to this question. progressDialogDisplayed = stateToRestore.getBoolean("progressDialogDisplayed"); Log.v(TAG + ".onRestoreInstanceState", "progressDialogDisplayed:" + progressDialogDisplayed); /*get the model data back, so that you can inform the model that the activity * has come back up. */ Object retained = getLastNonConfigurationInstance(); if (retained instanceof MonitorInstanceModel) { Log.v(TAG, "Restoring monitorinstancemodel on activity restore..."); monitorInstanceModel = (MonitorInstanceModel) retained; monitorInstanceModel.setActivity(this); } else if (retained instanceof CloudWatchMetricsModel) { Log.v(TAG, "Restoring metrics model on activity restore..."); metricsModel = (CloudWatchMetricsModel) retained; metricsModel.setActivity(this); } //restore the input data cloudWatchInput = (CloudWatchInput)(stateToRestore.getSerializable("cloudWatchInput")); //restore the measure data if any measureNames = stateToRestore.getStringArrayList("measureNames"); //restore the chart data multiRenderer = (XYMultipleSeriesRenderer) stateToRestore.getSerializable("multiRenderer"); dataset = (XYMultipleSeriesDataset) stateToRestore.getSerializable("dataset"); ((TextView)findViewById(R.id.monitorInstanceTextView)).setText(stateToRestore.getString( "titleText")); } /** * Executed last in the (re)awakening sequence. Gets Cloudwatch input data and either: * a) starts model, or * b) re-renders chart */ @Override public void onResume() { super.onResume(); //if there was a dialog box, display it //if failed, then display dialog box. Log.d(TAG + ".onResume()", "onResume"); if (alertDialogDisplayed) { alertDialogBox.setMessage(alertDialogMessage); alertDialogBox.show(); } else if ((cloudWatchInput == null) && (metricsModel == null) && (monitorInstanceModel == null)) { //this will execute the models as necessary! executeAllModels(); } //if dataset is not null, re-render the chart if (dataset != null) { Log.d(TAG, "Re-rendering charts"); //this will now add the chart into the layout.Whee! addChartToLayout(); } } /** * Save state of the activity on destroy/stop. * Apart from the usual, this saves: * <ul> * <li> cloudWatchInput: The cloudwatch input set (if any).</li> * <li> titleString: The title string for the graph being displayed. </li> * </ul> */ @Override public void onSaveInstanceState(Bundle saveState) { // if a dialog is displayed when this happens, dismiss it if (alertDialogDisplayed) { alertDialogBox.dismiss(); } //save the info as to whether dialog is displayed saveState.putBoolean("alertDialogDisplayed", alertDialogDisplayed); //save the dialog msg saveState.putString("alertDialogMessage", alertDialogMessage); //save if progress dialog is being displayed. saveState.putBoolean("progressDialogDisplayed", progressDialogDisplayed); if (cloudWatchInput != null) { saveState.putSerializable("cloudWatchInput", cloudWatchInput); } //save chart info if (chartView != null) { saveState.putSerializable("dataset", dataset); saveState.putSerializable("multiRenderer", multiRenderer); } if (measureNames != null) { saveState.putStringArrayList("measureNames", measureNames); } //save the title text saveState.putString("titleText", ((TextView)findViewById(R.id.monitorInstanceTextView)).getText().toString()); } /** * Save reference to {@link org.elasticdroid.model.MonitorInstanceModel} Async * Task when object is destroyed (for instance when screen rotated). * * This is done as the Async Task is running in the background. */ @Override public Object onRetainNonConfigurationInstance() { //save the monitor instance model if the model is running if (monitorInstanceModel != null) { Log.v(TAG, "Saving monitorinstancemodel on activity destroy..."); monitorInstanceModel.setActivityNull(); //tell the model the activity is going on hols. return monitorInstanceModel; } else if (metricsModel != null) { Log.v(TAG, "Saving metrics model on activity destroy..."); metricsModel.setActivityNull(); return metricsModel; } return null; } /** * Method that tries to get the default selections of the user from the DB. * If it can't find it in the DB, it uses the first measureName obtained from the metrics model. * If it can't find the measureNames, it calls the metricModel. When the MetricModel finishes, * it calls this method again. */ private void executeAllModels() { long timeNow = new Date().getTime(); long timeOneHrAgo = timeNow - 3600000; //subtract 3,600,000 milliseconds //we have no data in DB! if (measureNames == null) { executeMetricsModel(); } else { //it appears that no measures are available for instances stopped before AWS started //free basic monitoring, if they are started all of a sudden now. I am guessing this //is when this problem occurs. In this case, fail and return. if (measureNames.size() == 0 ) { Toast.makeText(this, this.getString(R.string.monitorinstanceview_nomeasures), Toast. LENGTH_LONG).show(); //dont try to plot the graph; just kill it. finish(); } cloudWatchInput = new CloudWatchInput(timeOneHrAgo, timeNow, new Integer(300), measureNames.get(0), "AWS/EC2", new ArrayList<String>(Arrays.asList(new String[]{"Average"})), selectedRegion); //Get defaults from DB try { cloudWatchInput = new ElasticDroidDB(this).getMonitoringDefaults(instanceId, selectedRegion); } catch (SQLException e) { // Error fetching from DB. //set CloudWatchInput to use CPUUtilization if present, or the first in the list //measure names if CPUUtilization isn't. if (measureNames.contains("CPUUtilization")) { cloudWatchInput = new CloudWatchInput(timeOneHrAgo, timeNow, new Integer(300), "CPUUtilization", "AWS/EC2", new ArrayList<String>( Arrays.asList(new String[]{"Average"})), selectedRegion); } else { cloudWatchInput = new CloudWatchInput(timeOneHrAgo, timeNow, new Integer(300), measureNames.get(0), "AWS/EC2", new ArrayList<String>( Arrays.asList(new String[]{"Average"})), selectedRegion); } //write this in as the default try { long retVal = new ElasticDroidDB(this).setMonitoringDefaults( connectionData.get("username"), instanceId, "instance", cloudWatchInput, false); if (retVal == -1) { Toast.makeText(this, this.getString(R.string. monitorinstanceview_cannotsave), Toast.LENGTH_LONG).show(); } } catch (SQLException exception) { Log.e(TAG, exception.getMessage()); Toast.makeText(this, "Could not save monitoring defaults to DB", Toast. LENGTH_LONG).show(); } } } } /** * Execute model to get list of valid metrics for this instance. */ private void executeMetricsModel() { //get the end point for the selected region and pass it to the model metricsModel = new CloudWatchMetricsModel(this, connectionData, selectedRegion); Dimension dimension = new Dimension(); dimension.setName("InstanceId"); dimension.setValue(instanceId); metricsModel.execute(dimension); } /** * Execute model to produce the chart for the measure selected. */ private void executeChartModel() { //create a dimension which specifies that we want to see data for this instance Dimension dimension = new Dimension(); dimension.setName("InstanceId"); dimension.setValue(instanceId); //create and start the model monitorInstanceModel = new MonitorInstanceModel(this, connectionData, cloudWatchInput); monitorInstanceModel.execute(dimension); } /** * Utility method called when the user touches "Refresh" * * It resets the end time to the current time, and the start time to endtime - duration * where duration = original end time - original start time */ private void refresh() { //there are no measurenames. This can happen if the user cancelled before measureNames //were received. if (measureNames == null) { executeMetricsModel(); //this will cause the chart model to be executed as well } else { //get the duration long duration = cloudWatchInput.getEndTime() - cloudWatchInput.getStartTime(); //get current time long currentTime =new Date().getTime(); cloudWatchInput.setEndTime(currentTime); //set end time to current time cloudWatchInput.setStartTime(currentTime - duration); //set start time to current time - //duration //call the model to get the values. executeChartModel(); } } /** * Process model results */ @SuppressWarnings("unchecked") @Override public void processModelResults(Object result) { if (progressDialogDisplayed) { progressDialogDisplayed = false; removeDialog(DialogConstants.PROGRESS_DIALOG.ordinal()); } //if result returned is null,d isplay a toast and do not try to re-execute the model //unless the user forces re-execution. if (result == null) { Toast.makeText(this, Html.fromHtml(this.getString(R.string.cancelled)), Toast. LENGTH_LONG).show(); return; //don't execute the rest of this method. } //due to a limitation in my code, multiple models can't be executed in parallel as we can't //then tell which one ran if (monitorInstanceModel != null) { monitorInstanceModel = null; //set the model to null if (result instanceof List<?>) { Log.v(TAG, "Drawing chart..."); //draw chart drawChart((List<Datapoint>) result); } else if (result instanceof AmazonServiceException) { // if a server error if (((AmazonServiceException) result).getErrorCode() .startsWith("5")) { alertDialogMessage = this.getString(R.string.loginview_server_err_dlg); } else { alertDialogMessage = this.getString(R.string.loginview_invalid_keys_dlg); } alertDialogDisplayed = true; killActivityOnError = false;//do not kill activity on server error //allow user to retry. } else if (result instanceof AmazonClientException) { alertDialogMessage = this .getString(R.string.loginview_no_connxn_dlg); alertDialogDisplayed = true; killActivityOnError = false;//do not kill activity on connectivity error. allow // client to retry. } } else if (metricsModel != null) { metricsModel = null; Log.v(TAG, "Metrics model returned..."); if (result instanceof ArrayList<?>) { measureNames = (ArrayList<String>) result; for (String measureName : measureNames) { Log.v(TAG, "Measure: "+ measureName); } //set the cloudwatch input defaults executeAllModels(); //now execute chart model Log.v(TAG, "Calling chart model..."); executeChartModel(); } else if (result instanceof AmazonServiceException) { // if a server error if (((AmazonServiceException) result).getErrorCode() .startsWith("5")) { alertDialogMessage = this.getString(R.string.loginview_server_err_dlg); } else { alertDialogMessage = this.getString(R.string.loginview_invalid_keys_dlg); } alertDialogDisplayed = true; killActivityOnError = true;//all errors should cause death of activity } else if (result instanceof AmazonClientException) { alertDialogMessage = this .getString(R.string.loginview_no_connxn_dlg); alertDialogDisplayed = true; killActivityOnError = true;//all errors should cause death of activity //to retry. } } //if failed, then display dialog box. if (alertDialogDisplayed) { alertDialogBox.setMessage(alertDialogMessage); alertDialogBox.show(); } } /** * Draw chart */ private void drawChart(List<Datapoint> cloudWatchData) { XYSeries cloudWatchSeries = new XYSeries(cloudWatchInput.getMeasureName()); //initialise the datasert and multi-series renderer if they are uninitialised ATM. if (dataset == null) { dataset = new XYMultipleSeriesDataset(); } if (multiRenderer == null) { multiRenderer = new XYMultipleSeriesRenderer(); } //remove all existing series. We are going to display only one at a time for (int seriesCount = 0; seriesCount < dataset.getSeriesCount(); seriesCount ++) { dataset.removeSeries(seriesCount); multiRenderer.removeSeriesRenderer(multiRenderer.getSeriesRendererAt(seriesCount)); } if (cloudWatchData.size() == 0) { Toast.makeText(this, this.getString(R.string.monitorinstanceview_nodata), Toast. LENGTH_LONG).show(); //dont try to plot the graph return; } for (Datapoint cloudWatchDatum : cloudWatchData) { //add the timestamp and the data cloudWatchSeries.add(cloudWatchDatum.getTimestamp().getTime(), cloudWatchDatum. getAverage()); } dataset.addSeries(cloudWatchSeries); XYSeriesRenderer renderer = new XYSeriesRenderer(); renderer.setColor(Color.RED); renderer.setPointStyle(PointStyle.CIRCLE); renderer.setLineWidth(5); multiRenderer.addSeriesRenderer(renderer); multiRenderer.setAntialiasing(true); multiRenderer.setYTitle(cloudWatchData.get(0).getUnit()); multiRenderer.setLabelsTextSize(16); multiRenderer.setAxisTitleTextSize(16); multiRenderer.setShowLegend(false); multiRenderer.setShowGrid(true); //set the title correctly TextView titleTextView = (TextView)findViewById(R.id.monitorInstanceTextView); long duration = cloudWatchInput.getEndTime() - cloudWatchInput.getStartTime(); if (duration == LAST_HOUR.getDuration()) { titleTextView.setText(cloudWatchInput.getMeasureName() + " (" + LAST_HOUR.getString( this) + ")"); } else if (duration == LAST_SIX_HOURS.getDuration()) { titleTextView.setText(cloudWatchInput.getMeasureName() + " (" + LAST_SIX_HOURS. getString(this) + ")"); } else if (duration == LAST_TWELVE_HOURS.getDuration()) { titleTextView.setText(cloudWatchInput.getMeasureName() + " (" + LAST_TWELVE_HOURS. getString(this) + ")"); } else if (duration == LAST_DAY.getDuration()) { titleTextView.setText(cloudWatchInput.getMeasureName() + " (" + LAST_DAY.getString(this) + ")"); } addChartToLayout(); //wasteful, but forceLayout does not work all the time (immediately) } /** * Utility method to add ChartView to their layout */ private void addChartToLayout() { if (chartView != null) { chartView.repaint(); } else { chartView = ChartFactory.getTimeChartView(this, dataset, multiRenderer, "HH:mm"); } LinearLayout layout = (LinearLayout) findViewById(R.id.chart); layout.removeAllViews(); layout.addView(chartView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); } /** * Overridden method to display the menu on press of the menu key * * Inflates and shows menu for displayed instances view. */ @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.monitorinstance_menu, menu); return true; } /** * Overriden. Prepares menu. Does nothing atm. */ @Override public boolean onPrepareOptionsMenu(Menu menu) { //does nothing now. return true; } /** * Overriden method to handle selection of menu item * Handles: * <ul> * <li>Refresh</li> * <li>Setting graph options : measure, duration </li> * </ul> */ @Override public boolean onOptionsItemSelected(MenuItem selectedItem) { switch (selectedItem.getItemId()) { case R.id.monitorinstance_menuitem_refresh: refresh(); return true; case R.id.monitorinstance_menuitem_about: Intent aboutIntent = new Intent(this, AboutView.class); startActivity(aboutIntent); return true; case R.id.monitorinstance_menuitem_graphtype: Intent optionsIntent = new Intent(this, MonitorInstanceOptionsView.class); //tell the activity what the currently selected measure and duration are optionsIntent.putExtra("selectedMeasureIdx", measureNames.indexOf(cloudWatchInput. getMeasureName())); optionsIntent.putExtra("selectedDuration", cloudWatchInput.getEndTime() - cloudWatchInput.getStartTime()); optionsIntent.putStringArrayListExtra("measureNames", measureNames); startActivityForResult(optionsIntent, 0); //second arg ignored return true; case R.id.monitorinstance_menuitem_watch: new ElasticDroidDB(this).updateMonitoringDefaults( new String[]{MonitorTbl.COL_WATCH}, new String[]{String.valueOf(1)}, //SQLite does not support booleans; so 1=true instanceId); //instance ID //set alert dialog box params alertDialogMessage = this.getString(R.string.monitorinstanceview_watch_alert); alertDialogDisplayed = true; killActivityOnError = false; //display alert dialog box alertDialogBox.setMessage(alertDialogMessage); alertDialogBox.show(); return true; default: return super.onOptionsItemSelected(selectedItem); } } /** * Handle back button. * If back button is pressed, UI should die. */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { //do not allow user to return to previous screen on pressing back button if (keyCode == KeyEvent.KEYCODE_BACK) { finish(); } return super.onKeyDown(keyCode, event); } /** * Called when the graph type selector activity returns. */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (resultCode) { case RESULT_OK: //set the start time, end time and measure name. if no data provided, then keep using //the old data cloudWatchInput.setStartTime(data.getLongExtra("startTime", cloudWatchInput. getStartTime())); cloudWatchInput.setEndTime(data.getLongExtra("endTime", cloudWatchInput. getEndTime())); cloudWatchInput.setMeasureName(data.getStringExtra("measureName")); //if this should be set as default, write to DB if (data.getBooleanExtra("setAsDefault", false)) { new ElasticDroidDB(this).updateMonitoringDefaults( new String[]{MonitorTbl.COL_DEFAULTDURATION, MonitorTbl. COL_DEFAULTMEASURENAME}, new String[]{String.valueOf(cloudWatchInput.getEndTime() - cloudWatchInput.getStartTime()), cloudWatchInput.getMeasureName()}, instanceId); } //execute the model to repopulate. executeChartModel(); break; } } /** * Handle the cancellation of the execution of the model. */ @Override public void onCancel(DialogInterface dialog) { progressDialogDisplayed = false; if (monitorInstanceModel != null) { monitorInstanceModel.cancel(true); } else if (metricsModel != null) { metricsModel.cancel(true); } } }